#include "Camera.h"

#include "MyGame.h"
#include "GFX.h"

// ...times the screen (visible) size
#define LOGIC_BORDER_FACTOR_X 3
#define LOGIC_BORDER_FACTOR_Y 3

Camera* Camera::m_camera = NULL;

Camera::Camera()
{
    shakeTimer = NULL;
    mapOffset = Vector2df(0,0);
    mapSubOffset = Vector2df(0.0,0.0);
    velocity = Vector2df(0,0);
    lastPos = Vector2df(0,0);
}

Camera::~Camera()
{
    delete shakeTimer;
}

Camera* Camera::GetSingleton()
{
    if (not m_camera)
        m_camera = new Camera();
    return m_camera;
}

Vector2df Camera::getMapOffset() const
{
    return mapOffset + mapSubOffset;
}

Vector2df Camera::move(Hero* const player)
{
    Vector2df diff = player->getPosition() - lastPos;
    Vector2df sets = diff;
    sets += checkScreenPlayer(player->getScreenPosition()+ Vector2df(player->getWidth() / 2, player->getHeight() / 2),diff);
    mapOffset += sets;
    lastPos = player->getPosition();

    MyGame::GetMyGame()->GetCurrentLevel()->RegisterSpawnCheck(getLogicTileRect());

    return sets;
}

void Camera::jumpTo(const Vector2df position)
{
    mapOffset = position;
}

void Camera::centerOnPlayer(const BaseUnit* const player)
{
    // center
    mapOffset = player->getPosition() + Vector2df(player->getWidth() / 2, player->getHeight() / 2) - Vector2df(400,240);

    // check C-tiles (quick and dirty - can most likely be optimized)
    MapData* data = MyGame::GetMyGame()->GetCurrentLevel()->tileMap;
    int size = data->getTileSize();
    Vector2df offset(0,0);
    int minX = floor(mapOffset.x / size);
    int maxX = ceil((mapOffset.x + GFX::getXResolution()) / size);
    int minY = floor(mapOffset.y / size);
    int maxY = ceil((mapOffset.y + GFX::getYResolution()) / size);
    for (int I = minX; I <= maxX; ++I) // check columns
    {
        offset.x = checkCTiles(data,I,I,minY,maxY,Vector2df(0,0));
        if (offset.x != 0) // break the check on the first found tiles
            break;
    }
    for (int I = minY; I <= maxY; ++I) // check rows
    {
        offset.y = checkCTiles(data,minX,maxX,I,I,Vector2df(0,0));
        if (offset.y != 0) // break the check on the first found tiles
            break;
    }
    mapOffset += offset;

    // check map bounds
    mapOffset += checkScreenPlayer(player->getScreenPosition()+ Vector2df(player->getWidth() / 2, player->getHeight() / 2),  Vector2df(0,0));

    lastPos = player->getPosition();

    MyGame::GetMyGame()->GetCurrentLevel()->RegisterSpawnCheck(getLogicTileRect());
}

void Camera::update()
{
    if (shakeTimer)
    {
        if (shakeTimer->getScaledTicks() >= 1)
        {
            shakeIndex++;
            if (shakeIndex < shakePattern.size())
            {
                mapSubOffset = shakePattern[shakeIndex];
                shakeTimer->start();
            }
            else
            {
                mapSubOffset = Vector2df(0.0,0.0);
                shakeIndex = 0;
                shakePattern.clear();
                delete shakeTimer;
                shakeTimer = NULL;
            }
        }
    }
}

void Camera::Shake(CRuint amount)
{
    vector<Vector2df> pattern;
    for (int I = 0; I < amount; ++I)
    {
        pattern.push_back(Vector2df(rand() % 11 - 5,rand() % 11 - 5)); // -5..5
    }
    Shake(pattern);
}

void Camera::Shake(vector<Vector2df> pattern)
{
    shakePattern = pattern;
    if (shakePattern.size() > 0)
    {
        if (!shakeTimer)
        {
            shakeTimer = new Timer();
            shakeTimer->setMode(MILLI_SECONDS);
        }
        shakeIndex = 0;
        mapSubOffset = shakePattern[0];
        shakeTimer->start();
    }
};

int Camera::getVisibleTileIndex(const SimpleDirection dir) const
{
    int size = MyGame::GetMyGame()->GetCurrentLevel()->tileMap->getTileSize();
    if (dir == diLEFT)
        return floor(mapOffset.x / size);
    else if (dir == diRIGHT)
        return ceil((mapOffset.x + GFX::getXResolution()) / size);
    else if (dir == diTOP)
        return floor(mapOffset.y / size);
    else if (dir == diBOTTOM)
        return ceil((mapOffset.y + GFX::getYResolution()) / size);
    else
        return -1;
};

int Camera::getLogicTileIndex(const SimpleDirection dir) const
{
    int size = MyGame::GetMyGame()->GetCurrentLevel()->tileMap->getTileSize();
    if (dir == diLEFT)
        return floor((mapOffset.x - GFX::getXResolution() * (LOGIC_BORDER_FACTOR_X - 1) / 2) / size);
    else if (dir == diRIGHT)
        return ceil((mapOffset.x + GFX::getXResolution() * ((LOGIC_BORDER_FACTOR_X - 1) / 2) + 1) / size);
    else if (dir == diTOP)
        return floor((mapOffset.y - GFX::getYResolution() * (LOGIC_BORDER_FACTOR_Y - 1) / 2) / size);
    else if (dir == diBOTTOM)
        return ceil((mapOffset.y + GFX::getYResolution() * ((LOGIC_BORDER_FACTOR_Y - 1) / 2) + 1)  / size);
    else
        return -1;
}

SDL_Rect Camera::getVisibleTileRect() const
{
    int size = MyGame::GetMyGame()->GetCurrentLevel()->tileMap->getTileSize();
    SDL_Rect rect;
    rect.x = floor(mapOffset.x / size);
    rect.y = floor(mapOffset.y / size);
    rect.w = ceil(GFX::getXResolution() / size);
    rect.h = ceil(GFX::getYResolution() / size);
    return rect;
}

SDL_Rect Camera::getLogicTileRect() const
{
    int size = MyGame::GetMyGame()->GetCurrentLevel()->tileMap->getTileSize();
    SDL_Rect rect;
    rect.x = floor((mapOffset.x - GFX::getXResolution() * (LOGIC_BORDER_FACTOR_X - 1) / 2) / size);
    rect.y = floor((mapOffset.y - GFX::getYResolution() * (LOGIC_BORDER_FACTOR_Y - 1) / 2) / size);
    rect.w = ceil(GFX::getXResolution() * LOGIC_BORDER_FACTOR_X / size);
    rect.h = ceil(GFX::getYResolution() * LOGIC_BORDER_FACTOR_Y / size);
    return rect;
}

///

Vector2df Camera::checkScreenPlayer(const Vector2df position, const Vector2df speed)
{
    MapData* data = MyGame::GetMyGame()->GetCurrentLevel()->tileMap;
    // pre-caching some values to speed up checks
    int size = data->getTileSize();
    Vector2df proPos = mapOffset + speed;
    Vector2df distance(0,0);
    if (speed.x > 0) // right
    {
        distance.x = min(data->getSize().x * size - proPos.x - GFX::getXResolution(),float(0));
        if (distance.x == 0)
        {
            //check for c-tiles on right side of the screen
            int x = floor((proPos.x + GFX::getXResolution()) / size) - 1;
            // no bounds check as getPropertyData does that
            int minY = floor(proPos.y / size);
            int maxY = ceil((proPos.y + GFX::getYResolution()) / size);
            distance.x = checkCTiles(data,x,x,minY,maxY,speed);
            if (distance.x == 0)
            {
                distance.x = max(min(position.x - GFX::getXResolution() / 2,float(0)),speed.x*-1);
            }
        }
    }
    else if (speed.x < 0) // left
    {
        // check for map bounds
        distance.x = max(0 - proPos.x,float(0));
        if (distance.x == 0)
        {
            // checking for c-tiles
            int x = floor(proPos.x / size) + 1;
            int minY = floor(proPos.y / size);
            int maxY = ceil((proPos.y + GFX::getYResolution()) / size);
            distance.x = checkCTiles(data,x,x,minY,maxY,speed);
            if (distance.x == 0)
            {
                // check for when player is not centered
                distance.x = min(max(position.x - GFX::getXResolution() / 2,float(0)),speed.x*-1);
            }
        }
    }
    else
    {
        distance.x = min(data->getSize().x * size - proPos.x - GFX::getXResolution(),float(0)) +
                     max(0 - proPos.x,float(0));
    }
    if (speed.y > 0) // down
    {
        distance.y = min(data->getSize().y*size - proPos.y - GFX::getYResolution(),float(0));
        if (distance.y == 0)
        {
            int y = floor((proPos.y + GFX::getYResolution()) / size) - 1;
            int minX = floor(proPos.x / size);
            int maxX = ceil((proPos.x + GFX::getXResolution()) / size);
            distance.y = checkCTiles(data,minX,maxX,y,y,speed);
            if (distance.y == 0)
            {
                distance.y = max(min(position.y - GFX::getYResolution() / 2,float(0)),speed.y*-1);
            }
        }
    }
    else if (speed.y < 0) // up
    {
        distance.y = max(0 - proPos.y,float(0));
        if (distance.y == 0)
        {
            int y = floor(proPos.y / size) + 1;
            int minX = floor(proPos.x / size);
            int maxX = ceil((proPos.x + GFX::getXResolution()) / size);
            distance.y = checkCTiles(data,minX,maxX,y,y,speed);
            if (distance.y == 0)
            {
                distance.y = min(max(position.y - GFX::getYResolution() / 2,float(0)),speed.y*-1);
            }
        }
    }
    else
    {
        distance.y = min(data->getSize().y*size - proPos.y - GFX::getYResolution(),float(0)) +
                     max(0 - proPos.y,float(0));
    }
    return distance;
}

float Camera::checkCTiles(const MapData* const data, CRint minX, CRint maxX, CRint minY, CRint maxY, const Vector2df speed)
{
    Vector2df proPos = mapOffset + speed;
    int size = data->getTileSize();
    Vector2df found(-1,-1);
    for (int X = minX; X <= maxX; ++X)
    {
        for (int Y = minY; Y <= maxY; ++Y)
        {
            if (data->getPropertyData(X,Y,GAME) == 1)
            {
                // we wait for a second tile to determine whether this is the right direction
                if (found == Vector2df(-1,-1))
                    found = Vector2df(X,Y);
                else
                {
                    // when a second tile with the property has been found we can be sure -> return value
                    if (found.x == X && found.y == Y-1) // vertical match
                    {
                        int posX = X * size;
                        if (posX <= mapOffset.x + GFX::getXResolution() / 2) // left side of the screen
                            return (posX - proPos.x);
                        else // right side of the screen
                            return posX + size - (proPos.x + GFX::getXResolution());
                    }
                    else if (found.y == Y && found.x == X-1) // horizontal match
                    {
                        int posY = Y * size;
                        if (posY <= mapOffset.y + GFX::getYResolution() / 2) // top of the screen
                            return (posY - proPos.y);
                        else // bottom of the screen
                            return posY + size - (proPos.y + GFX::getYResolution());
                    }
                    else // previous found was a single tile, which gets ignored
                        found = Vector2df(X,Y);
                }
            }
        }
    }
    return 0;
};
